package onem.soria.core;

import lombok.SneakyThrows;
import onem.soria.entity.MetaData;
import onem.soria.entity.constants.Constant;
import onem.soria.utils.DecryptUtil;
import onem.soria.utils.FileUtil;
import org.jaudiotagger.audio.AudioFile;
import org.jaudiotagger.audio.flac.FlacFileReader;
import org.jaudiotagger.audio.generic.AudioFileReader;
import org.jaudiotagger.audio.mp3.MP3FileReader;
import org.jaudiotagger.tag.FieldKey;
import org.jaudiotagger.tag.Tag;
import org.jaudiotagger.tag.images.StandardArtwork;
import org.tinylog.Logger;

import java.io.File;
import java.io.FileInputStream;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.logging.Level;

public class NcmDecrypt {
    private final File file;
    private final FileInputStream input;
    private String metaKey;

    static {
        // 禁用jaudiotagger日志
        java.util.logging.Logger.getLogger("org.jaudiotagger").setLevel(Level.OFF);
    }

    @SneakyThrows
    public NcmDecrypt(File file) {
        this.file = file;
        input = new FileInputStream(file);
    }

    @SneakyThrows
    public void decrypt() {
        Logger.info("Verify .ncm file...");
        verifyMagic();
        Logger.info(".ncm file verification succeeded.");
        FileUtil.skip(input, 2L);
        Logger.info("Decrypt RC4 key data and build key box...");
        // RC4-KSA
        byte[] keyData = getKeyData();
        Logger.info("Generate key stream...");
        // RC4-PRGA
        byte[] keyBox = buildKeyBox(keyData);
        Logger.info("Decrypt meta data and get image...");
        // 解密元数据
        MetaData meta = getMetaData();
        length();
        byte[] image = getImage();
        Logger.info("Decrypt music data and generate music file...");
        byte[] musicData = getMusicData(keyBox);
        // 生成音乐文件
        File musicFile = generate(meta, musicData);
        // 关闭输入流
        input.close();
        Logger.info("Change ID3tags...");
        // 修改ID3标签
        changeID3(musicFile, meta, image);
        Logger.info("Change succeeded.");
        Logger.info("Decrypt succeeded, enjoy your music!");
    }

    private int length() {
        byte[] keyLength = new byte[4];
        FileUtil.read(input, keyLength);
        return ByteBuffer.wrap(keyLength).order(ByteOrder.LITTLE_ENDIAN).asIntBuffer().get();
    }

    private void verifyMagic() {
        byte[] magic = new byte[8];
        FileUtil.read(input, magic);
        if (!Arrays.equals(magic, Constant.MAGIC)) {
            Logger.error("Incorrect MAGIC Code! Please check your file.");
        }
    }

    private byte[] getKeyData() {
        int len = length();
        byte[] keyData = new byte[len];
        FileUtil.read(input, keyData);
        for (int i = 0; i < keyData.length; i++) {
            keyData[i] ^= Constant.KEY_DATA;
        }
        return keyData;
    }

    private byte[] buildKeyBox(byte[] keyData) {
        byte[] origin = DecryptUtil.decryptCore(keyData, Constant.CORE_KEY);
        // remove "neteasecloudmusic"
        byte[] keyBox = Arrays.copyOfRange(origin, 17, origin.length);
        keyBox = DecryptUtil.generateKeyBox(keyBox);
        return keyBox;
    }

    private MetaData getMetaData() {
        int len = length();
        byte[] origin = new byte[len];
        FileUtil.read(input, origin);
        for (int i = 0; i < origin.length; i++) {
            origin[i] ^= Constant.META_DATA;
        }
        metaKey = new String(origin, StandardCharsets.UTF_8);
        // remove "163 key(don't modify)" 21 length
        byte[] originBase64 = Arrays.copyOfRange(origin, 22, origin.length);
        byte[] originCore = DecryptUtil.decryptBase64(originBase64);
        byte[] metaData = DecryptUtil.decryptCore(originCore, Constant.META_KEY);
        // remove "music:" 6 length
        metaData = Arrays.copyOfRange(metaData, 6, metaData.length);
        return new MetaData(metaData);
    }

    private byte[] getImage() {
        FileUtil.skip(input, 5L);
        int len = length();
        byte[] image = new byte[len];
        FileUtil.read(input, image);
        return image;
    }

    private byte[] getMusicData(byte[] keyBox) {
        byte[] musicData = new byte[(int) file.length()];
        int len = FileUtil.read(input, musicData);
        musicData = Arrays.copyOf(musicData, len);
        return DecryptUtil.generateKeyStream(musicData, keyBox);
    }

    @SneakyThrows
    private File generate(MetaData meta, byte[] musicData) {
        // 移除 "[]"
        String artistsName = Arrays.toString(meta.getArtists());
        artistsName = artistsName.substring(1, artistsName.length() - 1);
        String musicName = meta.getName();
        String musicFormat = meta.getFormat();
        String musicFileName = String.format("%s - %s.%s", artistsName, musicName, musicFormat);
        File musicFile = new File(file.getParent(), musicFileName);
        FileUtil.write(musicFile, musicData);
        Logger.info("Generate music file succeeded!");
        Logger.info("Music file path: " + musicFile.getAbsolutePath());
        return musicFile;
    }

    @SneakyThrows
    private void changeID3(File musicFile, MetaData meta, byte[] image) {
        String format = meta.getFormat();
        AudioFileReader reader = null;
        if ("flac".equals(format)) {
            reader = new FlacFileReader();
        }
        if ("mp3".equals(format)) {
            reader = new MP3FileReader();
        }
        if(reader == null) return;
        AudioFile file = reader.read(musicFile);
        file.createDefaultTag();
        Tag tag = file.getTag();
        tag.deleteArtworkField();
        tag.setField(FieldKey.TITLE, meta.getName());
        for (String artist : meta.getArtists()) {
            tag.addField(FieldKey.ARTIST, artist);
        }
        tag.setField(FieldKey.ALBUM, meta.getAlbum());
        tag.setField(FieldKey.COMMENT, metaKey);
        StandardArtwork artwork = new StandardArtwork();
        artwork.setBinaryData(image);
        artwork.setMimeType("image/jpeg");
        artwork.setPictureType(3);
        tag.setField(artwork);
        file.setTag(tag);
        file.commit();
    }
}
